<html>
    <head>
        <title>模块实体扩展系统</title>
    </head>
    <body>
        <script>
            // 模块实体扩展系统

                    /*
                        模块实体扩展系统是一个高级扩展系统，允许你为依赖模块的现有实体定义新属性。

                        它自动单点式想实体、数据库、HTTP API 和用户交互界面添加属性。

                        该模块的开发必须考虑到模块实体扩展系统。所有官方模块都尽可能支持这个系统。
                    */

                    /*
                        一、快速示例例

                                1、对实体进行扩展

                                        打开解决方案的Domain.Shared项目中的YourProjectNameModuleExtensionConfigurator类，并更改ConfigureExtraProperties方法，

                                        如下所示，将SocialSecurityNumber属性添加到Identity Module的IdentityUser实体。


                                                    public static void ConfigureExtraProperties()
                                                    {
                                                        OneTimeRunner.Run(() =>
                                                        {
                                                            ObjectExtensionManager.Instance.Modules()
                                                                .ConfigureIdentity(identity =>
                                                                {
                                                                    identity.ConfigureUser(user =>
                                                                    {
                                                                        user.AddOrUpdateProperty<string>( //property type: string
                                                                            "SocialSecurityNumber", //属性名
                                                                            property =>
                                                                            {
                                                                                // 验证规则
                                                                                property.Attributes.Add(new RequiredAttribute());
                                                                                property.Attributes.Add(
                                                                                    new StringLengthAttribute(64) {
                                                                                        MinimumLength = 4
                                                                                    }
                                                                                );

                                                                                //...other configurations for this property
                                                                            }
                                                                        );
                                                                    });
                                                                });
                                                        });
                                                    }

                                                    ObjectExtensionManager.Instance.Modules()是配置模块的起点。
                                                    
                                                    ConfigureIdentity(...)方法用于配置身份模块的实体。

                                                    identity.ConfigureUser(...)用于配置标识模块的用户实体。
                                                    
                                                    并非所有实体都设计为可扩展的（因为不需要）。使用智能感知来发现可扩展的模块和实体。

                                                    user.AddOrUpdateProperty<string>(...)用于为string类型的用户实体添加新属性
                                                （AddOrUpdateProperty方法可以针对同一实体的同一属性多次调用，每次调用可以配置同一属性的选项，但同一属性名的实体只添加一个属性）


                                                    SocialSecurityNumber是新属性的名称。

                                                    AddOrUpdateProperty获取第二个参数（property =>lambda表达式）来为新属性配置其他选项。

                                                            我们可以添加如下所示的数据注释属性，就像将数据注释属性添加到类属性一样。

                                2、创建和更新表单上自动发生变化：

                                        定义属性后，它会出现在相关实体的创建和更新表单中：

                                        SocialSecurityNumber字段进入表单。接下来的部分将解释这个新属性的本地化和验证。

                                3、数据表页面也有了变化：
                                                        
                                        新属性也出现在相关页面的数据表中：

                                        SocialSecurityNumber列进入表中。下一节将解释从数据表中隐藏此列的选项。
                    */

                    /*
                        二、用于配置属性的选项  Property Options

                                你可以在定义新属性时配置一些选项。

                                1、DisplayName 显示名称

                                        你可能希望为用户交互界面上显示的属性设置不同的（人类可读的）显示名称。

                                        ①、如果你不想本地化？

                                                如果你的应用程序没有本地化，你可以直接将属性的DisplayName设置为FixedLocalizableString对象。示例：

                                                        property =>
                                                        {
                                                            property.DisplayName = new FixedLocalizableString("Social security no");
                                                        }

                                        ②、本地化显示名称

                                                如果要本地化显示名称，有两个选项：

                                                    (1)、按照惯例本地化
                                                        
                                                            你可以直接打开本地化文件（如en.json）并将以下条目添加到texts部分，而不是设置property.DisplayName：

                                                                "SocialSecurityNumber": "Social security no"

                                                                在本地化文件中为你支持的每种语言定义相同的SocialSecurityNumber键（你之前定义的属性名称）。就是这么简单！

                                                            在某些情况下，本地化键可能与本地化文件中的其他键冲突。

                                                            在这种情况下，你可以使用DisplayName:前缀作为本地化文件中显示名称的前缀（DisplayName:SocialSecurityNumber作为本例的本地化键）。

                                                            扩展系统首先查找前缀版本，然后回退到无前缀名称（如果你没有本地化它，它会回退到属性名称）。

                                                            推荐这种方法，因为它简单且适用于大多数场景。

                                                    (2)、使用DispalyName属性进行本地化

                                                            如果要指定本地化密钥或本地化资源，仍然可以设置DisplayName选项：

                                                                        property =>
                                                                        {
                                                                            property.DisplayName =
                                                                                LocalizableString.Create<MyProjectNameResource>(
                                                                                    "UserSocialSecurityNumberDisplayName"
                                                                                );
                                                                        }

                                                                        MyProjectNameResource是本地化资源，UserSocialSecurityNumberDisplayName是本地化资源中的本地化键名。

                                2、默认值

                                        自动为新属性设置默认值，这是属性类型的自然默认值，例如string的null，false或int的0。

                                        有两种方法可以覆盖默认值：

                                            ①、DefaultValue选项

                                                    DefaultValue选项可以设置为任何值：

                                                            property =>
                                                            {
                                                                property.DefaultValue = 42;
                                                            }
                                                                
                                            ②、 DefaultValueFactory选项
                                                                                    
                                                     DefaultValueFactory可以设置为返回默认值的函数：
                                                     
                                                        property =>
                                                        {
                                                            property.DefaultValueFactory = () => DateTime.Now;
                                                        }

                                            options.DefaultValueFactory的优先级高于options.DefaultValue。

                                        
                                           提示：仅当默认值可能随时间变化时才使用DefaultValueFactory选项（如本例中的DateTime.Now）。如果它是一个常量值，则使用DefaultValue选项。


                                3、Validation 验证

                                        实体扩展系统允许你以多种方式定义扩展属性的验证。

                                        ①、数据注释特性

                                                            Attributes是与此属性关联的特性列表。

                                                            下面的示例代码将两个数据注释验证特性添加到属性中：

                                                                property =>
                                                                {
                                                                    property.Attributes.Add(new RequiredAttribute());
                                                                    property.Attributes.Add(new StringLengthAttribute(64) {MinimumLength = 4});
                                                                }

                                                        当你运行应用程序时，你会看到验证开箱即用：  表单上会有相关验证。

                                                        由于我们已经添加了RequiredAttribute，它不允许将其留空。验证系统工作：

                                                                在用户交互界面上（带有自动本地化）。

                                                                在HTTP API上。即使你直接执行HTTP请求，你也会收到带有正确HTTP状态代码的验证错误

                                                                在实体的SetProperty(...)方法上（如果你想知道SetProperty()方法是什么，请参阅文档）。https://abp.io/docs/latest/framework/architecture/domain-driven-design/entities

                                                        因此，它会自动进行全栈验证。
                                        
                                        ②、默认验证特性：

                                                  当你创建某些类型的属性时，会自动添加一些特性：

                                                        RequiredAttribute。如果要允许空值，请使属性为空（例如int?）。

                                                        EnumDataTypeAttribute，以防止设置无效的枚举值。

                                                  使用property.Attributes.Clear();如果你不想要这些特性。

                                         
                                        ③、验证操作

                                                  验证操作允许你执行自定义代码来执行验证。

                                                  下面的示例检查SocialSecurityNumber是否以B开头，如果是，则添加验证错误：

                                                        property =>
                                                        {
                                                            property.Attributes.Add(new RequiredAttribute());
                                                            property.Attributes.Add(new StringLengthAttribute(64) {MinimumLength = 4});

                                                            property.Validators.Add(context =>
                                                            {
                                                                if (((string) context.Value).StartsWith("B"))
                                                                {
                                                                    context.ValidationErrors.Add(
                                                                        new ValidationResult(
                                                                            "Social security number can not start with the letter 'B', sorry!",
                                                                            new[] {"extraProperties.SocialSecurityNumber"}
                                                                        )
                                                                    );
                                                                }
                                                            });

                                                        }
                                                  在这种情况下使用RegularExpressionAttribute可能更好，但这只是一个例子。

                                                  无论如何，如果你输入一个以字母B开头的值，你在保存表单时会收到以下错误：

                                                  ★、上下文对象：

                                                        context对象具有可用于自定义验证操作的有用属性。

                                                        例如，你可以使用context.ServiceProvider来解析来自依赖注入系统的服务。

                                                        下面的示例获取本地化程序并添加本地化错误消息：

                                                                if (((string) context.Value).StartsWith("B"))
                                                                {
                                                                    var localizer = context.ServiceProvider
                                                                        .GetRequiredService<IStringLocalizer<MyProjectNameResource>>();

                                                                    context.ValidationErrors.Add(
                                                                        new ValidationResult(
                                                                            localizer["SocialSecurityNumberCanNotStartWithB"],
                                                                            new[] {"extraProperties.SocialSecurityNumber"}
                                                                        )
                                                                    );
                                                                }

                                                    注意： context.ServiceProvider可以为空！只有当你在对象上使用SetProperty(...)方法时，它才能为null。

                                                           它才能为null。因为DI系统此时不可用。

                                                           虽然这是一种罕见的情况，但当context.ServiceProvider为null时，你应该执行兜底逻辑。

                                                           对于此示例，你将添加非本地化错误消息。

                                                           这不是问题，因为将无效值设置为属性通常是程序员的错误，在这种情况下你大多不需要本地化。

                                                           论如何，即使在常规属性设置器中，你也无法使用本地化。

                                                           但是，如果你对本地化很认真，你可以抛出业务异常（请参阅异常处理文档以了解如何本地化业务异常）。
                                 
                                4、UI 可见性

                                            当你定义一个属性时，它会出现在数据表上，在相关的UI页面上创建和编辑表单。但是，你可以单独控制每个。
                                            
                                            示例：
                                                    property =>
                                                    {
                                                        property.UI.OnTable.IsVisible = false;
                                                        //...other configurations
                                                    }

                                            使用property.UI.OnCreateForm和property.UI.OnEditForm来控制表单。

                                            。如果需要属性，但未添加到创建表单中，你肯定会得到验证异常，因此请小心使用此选项。
                                            
                                            但是如果你需要，编辑表单中可能没有必需的属性。

                                5、UI中显示的顺序

                                            当你定义一个属性时，它会出现在数据表上，在相关的UI页面上创建和编辑表单。但是，你可以控制它的顺序。
                                            
                                            示例：
                                                    property =>
                                                    {
                                                        property.UI.Order = 1;
                                                        //...other configurations
                                                    }
                                            使用property.UI.OnCreateForm和property.UI.OnEditForm来控制表单。

                                            如果需要属性，但未添加到创建表单中，你肯定会得到验证异常，因此请小心使用此选项。

                                            但是如果你需要，编辑表单中可能没有必需的属性。

                                6、HTTP API可用性

                                            即使你禁用了UI上的属性，它仍然可以通过HTTP API获得。默认情况下，所有API上都可以使用属性。

                                            使用property.Api选项使属性在某些API端点中不可用。

                                                property =>
                                                {
                                                    property.Api.OnUpdate.IsAvailable = false;
                                                }     
                                                    
                                            在此示例中，更新HTTP API将不允许为此属性设置新值。

                                            在这种情况下，你还希望在编辑表单上禁用此属性：

                                                property =>
                                                {
                                                    property.Api.OnUpdate.IsAvailable = false;
                                                    property.UI.OnEditForm.IsVisible = false;
                                                }

                                            除了property.Api.OnUpdate之外，你还可以设置property.Api.OnCreate和property.Api.OnGet以精细控制API端点。
                    */

                    /*
                        三、特殊类型

                                1、枚举、

                                            模块扩展系统自然支持enum类型。

                                            一个示例枚举类型：
                                                    public enum UserType
                                                    {
                                                        Regular,
                                                        Moderator,
                                                        SuperUser
                                                    }

                                            你可以像其他人一样添加枚举属性：

                                                    user.AddOrUpdateProperty<UserType>("Type");

                                            枚举属性在创建/编辑表单中显示为组合框（选择）：

                                            ★、枚举本地化

                                                    枚举成员名称默认显示在表和表单上。如果要本地化它，只需在localize文件上创建一个新条目：

                                                            "Enum:UserType.0": "Super user" 

                                                    以下名称之一可用作本地化键名（abp 内部约定）：

                                                            Enum:UserType.0
                                                            Enum:UserType.SuperUser
                                                            UserType.0
                                                            UserType.SuperUser
                                                            SuperUser

                                                    本地化系统按给定顺序搜索键。本地化文本用于表格和创建/编辑表单。

                                2、导航属性/外键

                                        支持将扩展属性添加到另一个实体（外键）的ID的实体。

                                        eg：  将部门与用户相关联

                                                ObjectExtensionManager.Instance.Modules()
                                                                        .ConfigureIdentity(identity =>
                                                                        {
                                                                            identity.ConfigureUser(user =>
                                                                            {
                                                                                user.AddOrUpdateProperty<Guid>(
                                                                                    "DepartmentId",
                                                                                    property =>
                                                                                    {
                                                                                        property.UI.Lookup.Url = "/api/departments";
                                                                                        property.UI.Lookup.DisplayPropertyName = "name";
                                                                                    }
                                                                                );
                                                                            });
                                                                        });

                                                UI.Lookup.Url选项采用URL来获取部门列表以在编辑/创建表单上进行选择。

                                                此端点可以是典型控制器、自动API控制器或返回正确JSON响应的任何类型的端点。

                                        返回固定部门列表的示例实现（在现实生活中，你从数据源获取列表）：
                                        
                                                [Route("api/departments")]
                                                public class DepartmentController : AbpController
                                                {
                                                    [HttpGet]
                                                    public async Task<ListResultDto<DepartmentDto>> GetAsync()
                                                    {
                                                        return new ListResultDto<DepartmentDto>(
                                                            new[]
                                                            {
                                                                new DepartmentDto
                                                                {
                                                                    Id = Guid.Parse("6267f0df-870f-4173-be44-d74b4b56d2bd"),
                                                                    Name = "Human Resources"
                                                                },
                                                                new DepartmentDto
                                                                {
                                                                    Id = Guid.Parse("21c7b61f-330c-489e-8b8c-80e0a78a5cc5"),
                                                                    Name = "Production"
                                                                }
                                                            }
                                                        );
                                                    }
                                                } 

                                        此Api 返回这样的Json响应：

                                                {
                                                    "items": [{
                                                        "id": "6267f0df-870f-4173-be44-d74b4b56d2bd",
                                                        "name": "Human Resources"
                                                    }, {
                                                        "id": "21c7b61f-330c-489e-8b8c-80e0a78a5cc5",
                                                        "name": "Production"
                                                    }]
                                                }

                                        ABP现在可以在创建或编辑用户时显示自动完成选择组件来选择部门：

                                        并在数据表上显示部门名称：


                                    ★、Lookup Options 查找选项：

                                                 UI.Lookup有以下选项来自定义如何读取从Url返回的响应：
                                                 
                                                 ①、Url：获取目标实体列表的端点。这用于编辑和创建表单。

                                                 ②、DisplayPropertyName：JSON响应中的属性，用于读取要在UI上显示的目标实体的显示名称。默认值：text。

                                                 ③、ValuePropertyName：JSON响应中读取目标实体ID的属性。默认值：id。

                                                 ④、FilterParamName：ABP允许在编辑/创建表单上搜索/过滤实体列表。
                                                   
                                                                     如果目标列表包含大量项目，这尤其有用。
                                                                     
                                                                     在这种情况下，你可以返回有限的列表（例如前100名）并允许用户在列表中搜索。
                                                                     
                                                                     ABP使用此选项的名称向服务器发送过滤文本（作为简单的查询字符串）。默认值：filter。

                                                 ⑤、ResultListPropertyName：
                                                        
                                                                    默认情况下，返回的JSON结果应包含items数组中的实体列表。你可以更改此字段的名称。默认值：items。


                                    ★、Lookup Properties: 是如何工作的

                                                你可能想知道ABP如何在上面的数据表上显示部门名称。

                                                很容易理解如何填写编辑和创建表单的下拉列表：ABP向给定的URL发出AJAX请求。每当用户键入以过滤项目时，它都会重新请求

                                                但是，对于数据表，UI上显示多个项目，执行单独的AJAX调用以获取每行的部门显示名称不会那么有效

                                                相反，除了外部实体的Id之外，外部实体的显示名称也保存为实体的额外属性（请参阅Entiies文档的Extra Properties部分）。
                                                
                                                如果检查数据库，你可以在数据库表的ExtraProperties字段中看到DepartmentId_Text：

                                                            {"DepartmentId":"21c7b61f-330c-489e-8b8c-80e0a78a5cc5","DepartmentId_Text":"Production"}

                                                因此，这是一种数据重复。如果你的目标实体名称稍后在数据库中发生更改，则没有自动同步系统。
                                                
                                                系统按预期工作，但你会在数据表上看到旧名称。如果这对你来说是个问题，你应该注意在实体显示名称更改时更新此信息。
                    */

                    /*
                        四、数据库映射

                                对于关系数据库，所有扩展属性值都存储在表中的单个字段中：

                                ExtraProperties字段将属性存储为JSON对象。
                                
                                虽然这在某些情况下很好，但你可能希望为你的新属性创建一个专用字段。

                                幸运的是，它很容易配置。

                                如果你使用的是Entity Framework Core数据库提供程序，你可以配置数据库映射，如下所示：

                                        ObjectExtensionManager.Instance
                                                              .MapEfCoreProperty<IdentityUser, string>(  // 配置扩展属性的数据库映射
                                                                    "SocialSecurityNumber",
                                                                    (entityBuilder, propertyBuilder) =>
                                                                    {
                                                                        propertyBuilder.HasMaxLength(64);
                                                                    }
                                                              );

                                在.EntityFrameworkCore项目中的YourProjectNameEfCoreEntityExtensionMappings类中写入此内容。

                                然后，你需要使用标准的Add-Migration和Update-Database命令来创建新的数据库迁移并将更改应用到你的数据库。

                                添加迁移创建一个新的迁移，如下所示：

                                        public partial class Added_SocialSecurityNumber_To_IdentityUser : Migration
                                        {
                                            protected override void Up(MigrationBuilder migrationBuilder)
                                            {
                                                migrationBuilder.AddColumn<string>(
                                                    name: "SocialSecurityNumber",
                                                    table: "AbpUsers",
                                                    maxLength: 128,
                                                    nullable: true);
                                            }

                                            protected override void Down(MigrationBuilder migrationBuilder)
                                            {
                                                migrationBuilder.DropColumn(
                                                    name: "SocialSecurityNumber",
                                                    table: "AbpUsers");
                                            }
                                        }
                                            
                                更新数据库后，你将看到AbpUsers表具有作为标准表字段的新属性：

                                备注：
                                     如果你首先创建了一个没有数据库表字段的属性，然后你需要将此属性移动到数据库表字段，建议在迁移中执行SQL命令以将旧值复制到新字段。

                                     但是，如果你没有成功，ABP会无缝管理它。它使用新的数据库字段，但如果为空，则回退到ExtraProperties字段。

                                     当你保存实体时，它会将值移动到新字段中。
                    */

                    /*
                        五、更多

                               有关所有可扩展性选项的总体索引，请参阅自定义模块指南。 

                               在这里，你可以做几件事：

                                      ①、  你可以创建第二个实体，该实体使用作为标准类属性的额外属性映射到同一个数据库表（如果你已经定义了EF Core映射）。

                                        对于上面的示例，你可以将public string SocialSecurityNumber {get; set;}属性添加到应用程序中的AppUser实体，因为AppUser实体映射到同一个AbpUser表。
                                        
                                        仅在需要时执行此操作，因为它会给你的应用程序带来更多复杂性。

                                      ②、 你可以覆盖域或应用程序服务以使用你的新属性执行自定义逻辑。

                                      ③、你可以低级控制如何在UI上的数据表中添加/呈现字段。
                    */



                    
        </script>
    </body>
</html>